From d7d0a974aad1ff44ee49feffe77b51e8183b953b Mon Sep 17 00:00:00 2001
From: Dave Stevenson <dave.stevenson@raspberrypi.com>
Date: Wed, 5 Jan 2022 19:14:48 +0000
Subject: [PATCH] drm/panel: Add panel driver for Ilitek ILI9806E panel

The Ilitek ILI9806E driver is used in the Pimoroni HyperPixel4
and potentially other displays. Whilst it can support multiple
interfaces, this driver only accounts for SPI configuration and
DPI video data.

Signed-off-by: Dave Stevenson <dave.stevenson@raspberrypi.com>
---
 drivers/gpu/drm/panel/Kconfig                 |  11 +
 drivers/gpu/drm/panel/Makefile                |   1 +
 drivers/gpu/drm/panel/panel-ilitek-ili9806e.c | 484 ++++++++++++++++++
 3 files changed, 496 insertions(+)
 create mode 100644 drivers/gpu/drm/panel/panel-ilitek-ili9806e.c

--- a/drivers/gpu/drm/panel/Kconfig
+++ b/drivers/gpu/drm/panel/Kconfig
@@ -174,6 +174,17 @@ config DRM_PANEL_ILITEK_ILI9341
 	  QVGA (240x320) RGB panels. support serial & parallel rgb
 	  interface.
 
+config DRM_PANEL_ILITEK_ILI9806E
+	tristate "Ilitek ILI9806E-based panels"
+	depends on OF && SPI
+	select DRM_KMS_HELPER
+	depends on DRM_GEM_CMA_HELPER
+	depends on BACKLIGHT_CLASS_DEVICE
+	select DRM_MIPI_DBI
+	help
+	  Say Y if you want to enable support for panels based on the
+	  Ilitek ILI9806e controller.
+
 config DRM_PANEL_ILITEK_ILI9881C
 	tristate "Ilitek ILI9881C-based panels"
 	depends on OF
--- a/drivers/gpu/drm/panel/Makefile
+++ b/drivers/gpu/drm/panel/Makefile
@@ -15,6 +15,7 @@ obj-$(CONFIG_DRM_PANEL_FEIXIN_K101_IM2BA
 obj-$(CONFIG_DRM_PANEL_FEIYANG_FY07024DI26A30D) += panel-feiyang-fy07024di26a30d.o
 obj-$(CONFIG_DRM_PANEL_ILITEK_IL9322) += panel-ilitek-ili9322.o
 obj-$(CONFIG_DRM_PANEL_ILITEK_ILI9341) += panel-ilitek-ili9341.o
+obj-$(CONFIG_DRM_PANEL_ILITEK_ILI9806E) += panel-ilitek-ili9806e.o
 obj-$(CONFIG_DRM_PANEL_ILITEK_ILI9881C) += panel-ilitek-ili9881c.o
 obj-$(CONFIG_DRM_PANEL_INNOLUX_EJ030NA) += panel-innolux-ej030na.o
 obj-$(CONFIG_DRM_PANEL_INNOLUX_P079ZCA) += panel-innolux-p079zca.o
--- /dev/null
+++ b/drivers/gpu/drm/panel/panel-ilitek-ili9806e.c
@@ -0,0 +1,484 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Ilitek ILI9806E TFT LCD drm_panel driver.
+ *
+ * Copyright (C) 2022 Raspberry Pi Ltd
+ *
+ * Derived from drivers/drm/gpu/panel/panel-sitronix-st7789v.c
+ * Copyright (C) 2017 Free Electrons
+ */
+
+#include <drm/drm_modes.h>
+#include <drm/drm_panel.h>
+
+#include <linux/bitops.h>
+#include <linux/gpio/consumer.h>
+#include <linux/media-bus-format.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/spi/spi.h>
+
+#include <video/mipi_display.h>
+#include <video/of_videomode.h>
+#include <video/videomode.h>
+
+struct ili9806 {
+	struct drm_panel panel;
+	struct spi_device *spi;
+	struct gpio_desc *reset;
+	struct regulator *power;
+	u32 bus_format;
+};
+
+#define ILI9806_DATA		BIT(8)
+
+#define ILI9806_MAX_MSG_LEN	6
+
+struct ili9806e_msg {
+	unsigned int len;
+	u16 msg[ILI9806_MAX_MSG_LEN];
+};
+
+#define ILI9806_SET_PAGE(page)	\
+	{				\
+		.len = 6,		\
+		.msg = {		\
+			0xFF,			\
+			ILI9806_DATA | 0xFF,	\
+			ILI9806_DATA | 0x98,	\
+			ILI9806_DATA | 0x06,	\
+			ILI9806_DATA | 0x04,	\
+			ILI9806_DATA | (page)	\
+		},				\
+	}
+
+#define ILI9806_SET_REG_PARAM(reg, data)	\
+	{					\
+		.len = 2,			\
+		.msg = {			\
+			(reg),			\
+			ILI9806_DATA | (data),	\
+		},				\
+	}
+
+#define ILI9806_SET_REG(reg)	\
+	{				\
+		.len = 1,		\
+		.msg = { (reg) },		\
+	}
+
+static const struct ili9806e_msg panel_init[] = {
+	ILI9806_SET_PAGE(1),
+
+	/* interface mode
+	 *   SEPT_SDIO = 0 (spi interface transfer through SDA pin)
+	 *   SDO_STATUS = 1 (always output, but without output tri-state)
+	 */
+	ILI9806_SET_REG_PARAM(0x08, 0x10),
+	/* display control
+	 * VSPL = 1 (vertical sync polarity)
+	 * HSPL = 0 (horizontal sync polarity)
+	 * DPL = 0 (PCLK polarity)
+	 * EPL = 1 (data enable polarity)
+	 */
+	ILI9806_SET_REG_PARAM(0x21, 0x0d),
+	/* resolution control (0x02 = 480x800) */
+	ILI9806_SET_REG_PARAM(0x30, 0x02),
+	/* display inversion control (0x00 = column inversion) */
+	ILI9806_SET_REG_PARAM(0x31, 0x00),
+	/* power control
+	 *  EXB1T = 0 (internal charge pump)
+	 *  EXT_CPCK_SEL = 1 (pump clock control signal = output 2 x waveform)
+	 *  BT = 0 (DDVDH / DDVDL voltage = VCI x 2 / VCI x -2)
+	 */
+	ILI9806_SET_REG_PARAM(0x40, 0x10),
+	/* power control
+	 *  DDVDH_CLP = 5.6 (DDVDH clamp leve)
+	 *  DDVDL_CLP = -5.6 (DDVDL clamp leve)
+	 */
+	ILI9806_SET_REG_PARAM(0x41, 0x55),
+	/* power control
+	 *  VGH_CP = 2DDVDH - DDVDL (step up factor for VGH)
+	 *  VGL_CP = DDVDL + VCL - VCIP (step up factor for VGL)
+	 */
+	ILI9806_SET_REG_PARAM(0x42, 0x02),
+	/* power control
+	 *  VGH_CLPEN = 0 (disable VGH clamp level)
+	 *  VGH_CLP = 9 (15.0 VGH clamp level - but this is disabled so not used?)
+	 */
+	ILI9806_SET_REG_PARAM(0x43, 0x84),
+	/* power control
+	 *  VGL_CLPEN = 0 (disable VGL clamp level)
+	 *  VGL_CLP = 9 (-11.0 VGL clamp level - but this is disabled so not used?)
+	 */
+	ILI9806_SET_REG_PARAM(0x44, 0x84),
+
+	/* power control
+	 *  VREG1OUT voltage for positive gamma?
+	 */
+	ILI9806_SET_REG_PARAM(0x50, 0x78),
+	/* power control
+	 *  VREG2OUT voltage for negative gamma?
+	 */
+	ILI9806_SET_REG_PARAM(0x51, 0x78),
+
+	ILI9806_SET_REG_PARAM(0x52, 0x00),
+	ILI9806_SET_REG_PARAM(0x53, 0x77),
+	ILI9806_SET_REG_PARAM(0x57, 0x60),
+	ILI9806_SET_REG_PARAM(0x60, 0x07),
+	ILI9806_SET_REG_PARAM(0x61, 0x00),
+	ILI9806_SET_REG_PARAM(0x62, 0x08),
+	ILI9806_SET_REG_PARAM(0x63, 0x00),
+	ILI9806_SET_REG_PARAM(0xA0, 0x00),
+	ILI9806_SET_REG_PARAM(0xA1, 0x07),
+	ILI9806_SET_REG_PARAM(0xA2, 0x0C),
+	ILI9806_SET_REG_PARAM(0xA3, 0x0B),
+	ILI9806_SET_REG_PARAM(0xA4, 0x03),
+	ILI9806_SET_REG_PARAM(0xA5, 0x07),
+	ILI9806_SET_REG_PARAM(0xA6, 0x06),
+	ILI9806_SET_REG_PARAM(0xA7, 0x04),
+	ILI9806_SET_REG_PARAM(0xA8, 0x08),
+	ILI9806_SET_REG_PARAM(0xA9, 0x0C),
+	ILI9806_SET_REG_PARAM(0xAA, 0x13),
+	ILI9806_SET_REG_PARAM(0xAB, 0x06),
+	ILI9806_SET_REG_PARAM(0xAC, 0x0D),
+	ILI9806_SET_REG_PARAM(0xAD, 0x19),
+	ILI9806_SET_REG_PARAM(0xAE, 0x10),
+	ILI9806_SET_REG_PARAM(0xAF, 0x00),
+	/* negative gamma control
+	 * set the gray scale voltage to adjust the gamma characteristics of the panel
+	 */
+	ILI9806_SET_REG_PARAM(0xC0, 0x00),
+	ILI9806_SET_REG_PARAM(0xC1, 0x07),
+	ILI9806_SET_REG_PARAM(0xC2, 0x0C),
+	ILI9806_SET_REG_PARAM(0xC3, 0x0B),
+	ILI9806_SET_REG_PARAM(0xC4, 0x03),
+	ILI9806_SET_REG_PARAM(0xC5, 0x07),
+	ILI9806_SET_REG_PARAM(0xC6, 0x07),
+	ILI9806_SET_REG_PARAM(0xC7, 0x04),
+	ILI9806_SET_REG_PARAM(0xC8, 0x08),
+	ILI9806_SET_REG_PARAM(0xC9, 0x0C),
+	ILI9806_SET_REG_PARAM(0xCA, 0x13),
+	ILI9806_SET_REG_PARAM(0xCB, 0x06),
+	ILI9806_SET_REG_PARAM(0xCC, 0x0D),
+	ILI9806_SET_REG_PARAM(0xCD, 0x18),
+	ILI9806_SET_REG_PARAM(0xCE, 0x10),
+	ILI9806_SET_REG_PARAM(0xCF, 0x00),
+
+	ILI9806_SET_PAGE(6),
+
+	ILI9806_SET_REG_PARAM(0x00, 0x20),
+	ILI9806_SET_REG_PARAM(0x01, 0x0A),
+	ILI9806_SET_REG_PARAM(0x02, 0x00),
+	ILI9806_SET_REG_PARAM(0x03, 0x00),
+	ILI9806_SET_REG_PARAM(0x04, 0x01),
+	ILI9806_SET_REG_PARAM(0x05, 0x01),
+	ILI9806_SET_REG_PARAM(0x06, 0x98),
+	ILI9806_SET_REG_PARAM(0x07, 0x06),
+	ILI9806_SET_REG_PARAM(0x08, 0x01),
+	ILI9806_SET_REG_PARAM(0x09, 0x80),
+	ILI9806_SET_REG_PARAM(0x0A, 0x00),
+	ILI9806_SET_REG_PARAM(0x0B, 0x00),
+	ILI9806_SET_REG_PARAM(0x0C, 0x01),
+	ILI9806_SET_REG_PARAM(0x0D, 0x01),
+	ILI9806_SET_REG_PARAM(0x0E, 0x00),
+	ILI9806_SET_REG_PARAM(0x0F, 0x00),
+	ILI9806_SET_REG_PARAM(0x10, 0xF0),
+	ILI9806_SET_REG_PARAM(0x11, 0xF4),
+	ILI9806_SET_REG_PARAM(0x12, 0x01),
+	ILI9806_SET_REG_PARAM(0x13, 0x00),
+	ILI9806_SET_REG_PARAM(0x14, 0x00),
+	ILI9806_SET_REG_PARAM(0x15, 0xC0),
+	ILI9806_SET_REG_PARAM(0x16, 0x08),
+	ILI9806_SET_REG_PARAM(0x17, 0x00),
+	ILI9806_SET_REG_PARAM(0x18, 0x00),
+	ILI9806_SET_REG_PARAM(0x19, 0x00),
+	ILI9806_SET_REG_PARAM(0x1A, 0x00),
+	ILI9806_SET_REG_PARAM(0x1B, 0x00),
+	ILI9806_SET_REG_PARAM(0x1C, 0x00),
+	ILI9806_SET_REG_PARAM(0x1D, 0x00),
+	ILI9806_SET_REG_PARAM(0x20, 0x01),
+	ILI9806_SET_REG_PARAM(0x21, 0x23),
+	ILI9806_SET_REG_PARAM(0x22, 0x45),
+	ILI9806_SET_REG_PARAM(0x23, 0x67),
+	ILI9806_SET_REG_PARAM(0x24, 0x01),
+	ILI9806_SET_REG_PARAM(0x25, 0x23),
+	ILI9806_SET_REG_PARAM(0x26, 0x45),
+	ILI9806_SET_REG_PARAM(0x27, 0x67),
+	ILI9806_SET_REG_PARAM(0x30, 0x11),
+	ILI9806_SET_REG_PARAM(0x31, 0x11),
+	ILI9806_SET_REG_PARAM(0x32, 0x00),
+	ILI9806_SET_REG_PARAM(0x33, 0xEE),
+	ILI9806_SET_REG_PARAM(0x34, 0xFF),
+	ILI9806_SET_REG_PARAM(0x35, 0xBB),
+	ILI9806_SET_REG_PARAM(0x36, 0xAA),
+	ILI9806_SET_REG_PARAM(0x37, 0xDD),
+	ILI9806_SET_REG_PARAM(0x38, 0xCC),
+	ILI9806_SET_REG_PARAM(0x39, 0x66),
+	ILI9806_SET_REG_PARAM(0x3A, 0x77),
+	ILI9806_SET_REG_PARAM(0x3B, 0x22),
+	ILI9806_SET_REG_PARAM(0x3C, 0x22),
+	ILI9806_SET_REG_PARAM(0x3D, 0x22),
+	ILI9806_SET_REG_PARAM(0x3E, 0x22),
+	ILI9806_SET_REG_PARAM(0x3F, 0x22),
+	ILI9806_SET_REG_PARAM(0x40, 0x22),
+	/* register doesn't exist on page 6? */
+	ILI9806_SET_REG_PARAM(0x52, 0x10),
+	/* doesn't make sense, not valid according to datasheet */
+	ILI9806_SET_REG_PARAM(0x53, 0x10),
+	/* doesn't make sense, not valid according to datasheet */
+	ILI9806_SET_REG_PARAM(0x54, 0x13),
+
+	ILI9806_SET_PAGE(7),
+
+	/* enable VREG */
+	ILI9806_SET_REG_PARAM(0x18, 0x1D),
+	/* enable VGL_REG */
+	ILI9806_SET_REG_PARAM(0x17, 0x22),
+	/* register doesn't exist on page 7? */
+	ILI9806_SET_REG_PARAM(0x02, 0x77),
+	/* register doesn't exist on page 7? */
+	ILI9806_SET_REG_PARAM(0x26, 0xB2),
+	/* register doesn't exist on page 7? */
+	ILI9806_SET_REG_PARAM(0xE1, 0x79),
+
+	ILI9806_SET_PAGE(0),
+
+	ILI9806_SET_REG_PARAM(MIPI_DCS_SET_PIXEL_FORMAT,
+			      MIPI_DCS_PIXEL_FMT_18BIT << 4),
+	ILI9806_SET_REG_PARAM(MIPI_DCS_SET_TEAR_ON, 0x00),
+	ILI9806_SET_REG(MIPI_DCS_EXIT_SLEEP_MODE),
+};
+
+#define NUM_INIT_REGS ARRAY_SIZE(panel_init)
+
+static inline struct ili9806 *panel_to_ili9806(struct drm_panel *panel)
+{
+	return container_of(panel, struct ili9806, panel);
+}
+
+static int ili9806_write_msg(struct ili9806 *ctx, const struct ili9806e_msg *msg)
+{
+	struct spi_transfer xfer = { };
+	struct spi_message spi;
+	//u16 txbuf[] = { msg->, ILI9806_DATA | data };
+
+	spi_message_init(&spi);
+
+	xfer.tx_buf = msg->msg;
+	xfer.bits_per_word = 9;
+	xfer.len = sizeof(u16) * msg->len;
+
+	spi_message_add_tail(&xfer, &spi);
+	return spi_sync(ctx->spi, &spi);
+}
+
+static int ili9806e_write_msg_list(struct ili9806 *ctx,
+				   const struct ili9806e_msg msgs[],
+				   unsigned int num_msgs)
+{
+	int ret, i;
+
+	for (i = 0; i < num_msgs; i++) {
+		ret = ili9806_write_msg(ctx, &msgs[i]);
+		if (ret)
+			break;
+	}
+
+	return ret;
+}
+
+static const struct drm_display_mode ili9806e_480x800_mode = {
+	.clock = 32000,
+	.hdisplay = 480,
+	.hsync_start = 480 + 10,
+	.hsync_end = 480 + 10 + 16,
+	.htotal = 480 + 10 + 16 + 59,
+	.vdisplay = 800,
+	.vsync_start = 800 + 15,
+	.vsync_end = 800 + 15 + 113,
+	.vtotal = 800 + 15 + 113 + 15,
+	.flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC,
+};
+
+static int ili9806_get_modes(struct drm_panel *panel,
+			     struct drm_connector *connector)
+{
+	struct ili9806 *ctx = panel_to_ili9806(panel);
+	struct drm_display_mode *mode;
+
+	mode = drm_mode_duplicate(connector->dev, &ili9806e_480x800_mode);
+	if (!mode) {
+		dev_err(panel->dev, "failed to add mode %ux%ux@%u\n",
+			ili9806e_480x800_mode.hdisplay,
+			ili9806e_480x800_mode.vdisplay,
+			drm_mode_vrefresh(&ili9806e_480x800_mode));
+		return -ENOMEM;
+	}
+
+	drm_mode_set_name(mode);
+
+	mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;
+	drm_mode_probed_add(connector, mode);
+
+	connector->display_info.width_mm = 61;
+	connector->display_info.height_mm = 103;
+	drm_display_info_set_bus_formats(&connector->display_info,
+					 &ctx->bus_format, 1);
+	connector->display_info.bus_flags =
+					DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE;
+
+	return 1;
+}
+
+static int ili9806_prepare(struct drm_panel *panel)
+{
+	struct ili9806 *ctx = panel_to_ili9806(panel);
+	int ret;
+
+	ret = regulator_enable(ctx->power);
+	if (ret)
+		return ret;
+
+	ret = ili9806e_write_msg_list(ctx, panel_init, NUM_INIT_REGS);
+
+	return ret;
+}
+
+static int ili9806_enable(struct drm_panel *panel)
+{
+	struct ili9806 *ctx = panel_to_ili9806(panel);
+	const struct ili9806e_msg msg = ILI9806_SET_REG(MIPI_DCS_SET_DISPLAY_ON);
+	int ret;
+
+	ret = ili9806_write_msg(ctx, &msg);
+
+	return ret;
+}
+
+static int ili9806_disable(struct drm_panel *panel)
+{
+	struct ili9806 *ctx = panel_to_ili9806(panel);
+	const struct ili9806e_msg msg = ILI9806_SET_REG(MIPI_DCS_SET_DISPLAY_OFF);
+	int ret;
+
+	ret = ili9806_write_msg(ctx, &msg);
+
+	return ret;
+}
+
+static int ili9806_unprepare(struct drm_panel *panel)
+{
+	struct ili9806 *ctx = panel_to_ili9806(panel);
+	const struct ili9806e_msg msg = ILI9806_SET_REG(MIPI_DCS_ENTER_SLEEP_MODE);
+	int ret;
+
+	ret = ili9806_write_msg(ctx, &msg);
+
+	return ret;
+}
+
+static const struct drm_panel_funcs ili9806_drm_funcs = {
+	.disable	= ili9806_disable,
+	.enable		= ili9806_enable,
+	.get_modes	= ili9806_get_modes,
+	.prepare	= ili9806_prepare,
+	.unprepare	= ili9806_unprepare,
+};
+
+static const struct of_device_id ili9806_of_match[] = {
+	{	.compatible = "txw,txw397017s2",
+		.data = (void *)MEDIA_BUS_FMT_RGB888_1X24,
+	}, {
+		.compatible = "pimoroni,hyperpixel4",
+		.data = (void *)MEDIA_BUS_FMT_RGB666_1X24_CPADHI,
+	}, {
+		.compatible = "ilitek,ili9806e",
+		.data = (void *)MEDIA_BUS_FMT_RGB888_1X24,
+	}, {
+		/* sentinel */
+	}
+};
+MODULE_DEVICE_TABLE(of, ili9806_of_match);
+
+static int ili9806_probe(struct spi_device *spi)
+{
+	const struct ili9806e_msg panel_reset[] = {
+		ILI9806_SET_PAGE(0),
+		ILI9806_SET_REG_PARAM(0x01, 0x00)
+	};
+	const struct of_device_id *id;
+	struct ili9806 *ctx;
+	int ret;
+
+	ctx = devm_kzalloc(&spi->dev, sizeof(*ctx), GFP_KERNEL);
+	if (!ctx)
+		return -ENOMEM;
+
+	id = of_match_node(ili9806_of_match, spi->dev.of_node);
+	if (!id)
+		return -ENODEV;
+
+	ctx->bus_format = (u32)(uintptr_t)id->data;
+
+	spi_set_drvdata(spi, ctx);
+	ctx->spi = spi;
+
+	drm_panel_init(&ctx->panel, &spi->dev, &ili9806_drm_funcs,
+		       DRM_MODE_CONNECTOR_DPI);
+
+	ctx->power = devm_regulator_get(&spi->dev, "power");
+	if (IS_ERR(ctx->power))
+		return PTR_ERR(ctx->power);
+
+	ctx->reset = devm_gpiod_get_optional(&spi->dev, "reset", GPIOD_OUT_LOW);
+	if (IS_ERR(ctx->reset)) {
+		dev_err(&spi->dev, "Couldn't get our reset line\n");
+		return PTR_ERR(ctx->reset);
+	}
+
+	/* Soft reset */
+	ili9806e_write_msg_list(ctx, panel_reset, ARRAY_SIZE(panel_reset));
+	msleep(200);
+
+	ret = drm_panel_of_backlight(&ctx->panel);
+	if (ret)
+		return ret;
+
+	drm_panel_add(&ctx->panel);
+
+	return 0;
+}
+
+static void ili9806_remove(struct spi_device *spi)
+{
+	struct ili9806 *ctx = spi_get_drvdata(spi);
+
+	drm_panel_remove(&ctx->panel);
+}
+
+static const struct spi_device_id ili9806_ids[] = {
+	{ "txw397017s2", 0 },
+	{ "ili9806e", 0 },
+	{ "hyperpixel4", 0 },
+	{ /* sentinel */ }
+};
+
+MODULE_DEVICE_TABLE(spi, ili9806_ids);
+
+static struct spi_driver ili9806_driver = {
+	.probe = ili9806_probe,
+	.remove = ili9806_remove,
+	.driver = {
+		.name = "ili9806e",
+		.of_match_table = ili9806_of_match,
+	},
+	.id_table = ili9806_ids,
+};
+module_spi_driver(ili9806_driver);
+
+MODULE_AUTHOR("Dave Stevenson <dave.stevenson@raspberrypi.com>");
+MODULE_DESCRIPTION("ili9806 LCD panel driver");
+MODULE_LICENSE("GPL v2");
